<?php

/*
 * Copyright (C) 2009 - 2011 Pham Cong Dinh
 *
 * This file is part of Spica.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

/**
 * The class is responsible for mapping urls / routes to module/controller and
 * its methods.
 *
 * namespace spica\core\RegexUrlResolver
 *
 * @category   spica
 * @package    core
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 29, 2009
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: RegexUrlResolver.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaRegexUrlResolver implements SpicaPathResolver
{
    /**
     * Profile routing config.
     *
     * @var array
     */
    protected static $_config;

    /**
     * Path to execution script file, relative to DocumentRoot of the webserver.
     * Its value is often the same as $_SERVER['SCRIPT_NAME'] for the most cases.
     * However, it is good to separate it with $_SERVER['SCRIPT_NAME'] to make unit
     * testing easier. E.x: /repos/spica/trunk/index.php where /repos is a
     * directory in DocumentRoot configured by webserver.
     *
     * @var string
     */
    protected static $_scriptPath;

    /**
     * Configured routes.
     *
     * @var array
     */
    protected $_routes = array();

    /**
     * Compiled static routes.
     *
     * @var array
     */
    protected $_staticRoutes = array();

    /**
     * Compiled dynamic routes.
     *
     * @var array
     */
    protected $_dynamicRoutes = array();

    /**
     * Matched route.
     *
     * @var array
     */
    protected $_matched = array();

    /**
     * Fallback resolver.
     *
     * @var SpicaPathResolver
     */
    protected $_fallbackResolver;

    /**
     * Constructs an object of <code>SpicaRegexUrlResolver</code>.
     *
     * @param array  $config     Context configuration
     * @param string $scriptPath Relative request script path.
     */
    public function __construct($config, $scriptPath)
    {
        self::$_config     = $config;
        self::$_scriptPath = $scriptPath;
    }

    /**
     * (non-PHPdoc)
     * @see trunk/library/spica/core/SpicaPathResolver#setFallbackResolver()
     */
    public function setFallbackResolver($resolver)
    {
        $this->_fallbackResolver = $resolver;
    }

    /**
     * (non-PHPdoc)
     * @see trunk/library/spica/core/SpicaPathResolver#getFallbackResolver()
     */
    public function getFallbackResolver()
    {
        return $this->_fallbackResolver;
    }

    /**
     * Connects a route name to an URL pattern.
     *
     * Additionally, attach an action to a route, and impose some
     * restrictions to route arguments.
     *
     * @param string $name A name represents a particular route.
     * @param string $type Route type: dynamic and static
     * @param string $schema Route URL pattern, e.g. 'category/:arg/*'
     * @param array  $map  An array that maps package, module, controller, action
     *                     and other parameters to specific values when the route is matched
     * @param array  $requirements array of requirements for route arguments,
     *               keys are route argument names, values are regular expressions
     */
    public function addRoute($name, $type, $schema, $map, $requirements = array())
    {
        $this->_routes[$type][] = array(
          'name'    => $name,
          'pattern' => $schema,
          'map'     => $map,
          'req'     => $requirements,
        );
    }

    /**
     * Resolves URLs into parts and populates the global $_GET.
     *
     * The following indices of $_GET will be populated after that
     * + controller
     * + module
     * + action
     * + page
     * + other params
     *
     * Unescape/urldecode $url as neccessary before matching.
     *
     * @param  string $url
     * @return string|false Route name that matches with the request URL or false
     */
    public function resolve($url)
    {
        $relativeUrl = parse_url($url, PHP_URL_PATH); // has leading /
        // Removes file part and leading and trailing slash. Can be empty
        $subDirectoryPath = trim(substr(self::$_scriptPath, 0, -9), '/');
        // Remove sub directory path and / at both ends
        $relativeUrl = ltrim(str_replace($subDirectoryPath, '', $relativeUrl), '/');

        $this->compile();
        $map = $this->matchStaticRoutes($relativeUrl);

        if (0 === count($map))
        {
            $map = $this->matchDynamicRoutes($relativeUrl);
        }

        if (0 === count($map))
        {
            if (null !== $this->_fallbackResolver)
            {
                return $this->_fallbackResolver->resolve($url);
            }

            return false;
        }

        $this->_matched = $map;
        $_GET['_module']     = isset($map['map']['_module']) ? $map['map']['_module'] : (isset($map['map']['module']) ? $map['map']['module'] : self::$_config['default_module']);
        $_GET['_controller'] = isset($map['map']['_controller']) ? $map['map']['_controller'] : (isset($map['map']['controller']) ? $map['map']['controller'] : self::$_config['default_controller']);
        $_GET['_action']     = isset($map['map']['_action']) ? $map['map']['_action'] : (isset($map['map']['action']) ? $map['map']['action'] : self::$_config['default_action']);

        unset($map['map']['module']);
        unset($map['map']['controller']);
        unset($map['map']['action']);

        unset($map['map']['_module']);
        unset($map['map']['_controller']);
        unset($map['map']['_action']);

        $_GET = array_merge($map['map'], $_GET);

        if (isset($_GET['__any__string__']))
        {
            $params = explode('/', $_GET['__any__string__']);

            for ($i = 0, $length = count($params); $i < $length; $i++)
            {
                if ('' === trim($params[$i]))
                {
                    continue; // loop util the first real parameter is met
                }

                if (true   === isset($params[$i]))
                {
                    $key   = $params[$i];
                    $value = (true === isset($params[$i + 1]))?$params[$i + 1]:'';
                    if ('%5B%5D' == substr($key, -6))
                    {
                        $key     = str_replace('%5B%5D', '', $key);
                        $_GET[$key][] = $value;
                    }
                    else
                    {
                        $_GET[$key]   = $value;
                    }
                }

                $i++;
            }

            unset($_GET['__any__string__']);
        }

        return $map['name'];
    }

    /**
     * Compiles Spica routing pattern into Perl-compatible regex ones.
     */
    public function compile()
    {
        include_once 'library/spica/core/utils/ArrayUtils.php';

        if (isset($this->_routes['static']))
        {
            // Longer URL patterns are proccessed first.
            SpicaArrayUtils::kvrsort($this->_routes['static'], 'pattern');
            $this->_compileStaticRoutes($this->_routes['static']);
        }

        if (isset($this->_routes['dynamic']))
        {
            // Longer URL patterns are proccessed first.
            SpicaArrayUtils::kvrsort($this->_routes['dynamic'], 'pattern');
            $this->_compileDynamicRoutes($this->_routes['dynamic']);
        }
    }

    /**
     * Gets the matched route name.
     *
     * @return string The route name
     */
    public function getMatchedRouteName()
    {
        return isset($this->_matched['name']) ? $this->_matched['name'] : null;
    }

    /**
     * Compiles a set of routes into regex patterns and its associated meta data.
     *
     * @param array $routes
     */
    protected function _compileDynamicRoutes($routes)
    {
        foreach ($routes as $route)
        {
            $pattern   = ltrim($route['pattern'], '/');
            $processed = preg_replace(array('@:([a-z]+)@', '@(\*)@'), array('(?P<$1>[\w\d_]+)', '(?P<__any__string__>.*)'), $pattern);

            // Not end with '*'
            if ('*' !== substr($pattern, -1, 1))
            {
                $processed = rtrim($processed, '/').'$';
            }

            $this->_dynamicRoutes[] = array(
              'name' => $route['name'],
              'p'    => '@^'.rtrim($processed, '$').'$@',
              'map'  => $route['map'],
              'req'  => $route['req'],
            );
        }
    }

    /**
     * Compiles a set of routes into regex patterns and its associated meta data.
     *
     * @param array $routes
     */
    protected function _compileStaticRoutes($routes)
    {
        foreach ($routes as $route)
        {
            $pattern = ltrim($route['pattern'], '/');
            $this->_staticRoutes[] = array(
              'name' => $route['name'],
              'p'    => '@^'.rtrim($pattern, '$').'$@',
              'map'  => $route['map'],
              'req'  => $route['req'],
            );
        }
    }

    /**
     * Matches static routes.
     *
     * @param  string $requestUrl
     * @return array  Route map array or empty array
     */
    public function matchStaticRoutes($requestUrl)
    {
        foreach ($this->_staticRoutes as $map)
        {
            if ($requestUrl === $map['p'])
            {
                return $map;
            }
        }

        return array();
    }

    /**
     * Matches dynamic routes.
     *
     * @param  string $requestUrl
     * @return array Route map array or empty array.
     */
    public function matchDynamicRoutes($requestUrl)
    {
        foreach ($this->_dynamicRoutes as $map)
        {
            if (preg_match_all($map['p'], $requestUrl, $matches))
            {
                $matched    = spica_keep_assoc_key($matches);
                $map['map'] = array_merge($map['map'], $matched);
                return $map;
            }
        }

        return array();
    }
}

/**
 * Filters provided array to produce a new array that contains entries with associative keys only.
 *
 * @param  array $array
 * @return array
 */
function spica_keep_assoc_key($array)
{
    foreach ($array as $key => $value)
    {
        if (is_numeric($key))
        {
            unset($array[$key]);
        }
        else
        {
            // named pattern match
            $array[$key] = $value[0];
        }
    }

    return $array;
}

?>